/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.appspot.trick.it.admin.api;

import com.appspot.trick.it.admin.api.exception.CodeExchangeException;
import com.appspot.trick.it.admin.db.OauthMstEntity;
import com.appspot.trick.it.admin.db.DBManager;
import static com.appspot.trick.it.admin.db.DBManager.getConn;
import com.appspot.trick.it.admin.model.Oauth2Model;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URL;
import java.sql.Connection;
import java.util.Arrays;
import java.util.List;

public class GoogleOauthAPIService {
    
    /**
     * Google 承認情報
     */
    private GoogleClientSecrets googleClientSecrets;
    
    /**
     * アプリケーションスコープ
     */
    private static final List<String> SCOPES = Arrays.asList(
            "https://www.googleapis.com/auth/drive",
            "https://www.googleapis.com/auth/userinfo.email",
            "https://www.googleapis.com/auth/userinfo.profile");
    /**
     * Google OAuth Flow
     */
    private static GoogleAuthorizationCodeFlow flow = null;
    
    /**
     * コンストラクタ
     * @throws java.io.IOException
     */
    public GoogleOauthAPIService() throws IOException {
        
        getGoogleClientSecrets();
    }
    
    /**
     * ClientSecretsの取得
     * @param jsonFactory
     * @return GoogleClientSecrets
     * @throws IOException 
     */
    private void getGoogleClientSecrets() throws IOException {
//        FileInputStream is = new FileInputStream(Constants.APP_SECRETS_DIR_PATH + "/" + appId + ".json");
//        Reader reader = new InputStreamReader(is);
        
        Reader reader = new InputStreamReader(GoogleOauthAPIService.class.getResourceAsStream("client_secrets.json"));
        
        this.googleClientSecrets =  GoogleClientSecrets.load(new JacksonFactory(), reader);
    }

    /**
     * DBから承認情報を取得する
     *
     * @return Credential
     * @throws java.lang.Exception
     */
    public Credential getCredentials() throws Exception {

        Connection conn = getConn();
        
        // DBから承認情報を取得
        OauthMstEntity entity = DBManager.getOauthEntity(conn);
        // DBに存在しない場合はNULLを返す
        if(entity == null) return null;
        
        // アクセストークンの有効チェック
        if (isInvalidToken(entity.accessToken)) {
            // アクセストークンが無効な場合は再取得し、DBを更新
            String newAccess = getNewAccessTokenByRefreshToken(entity);
            if (newAccess != null) { // アクセストールンがまだ有効の場合はNULLが返される
                entity.accessToken = newAccess;
                DBManager.updateOauthEntity(conn, entity);
            }
        }else {
            System.out.println("有効なアクセストークン:" + entity.accessToken);
        }
        
        // clientSecretsの取得
        HttpTransport httpTransport = new NetHttpTransport();
        JacksonFactory jsonFactory = new JacksonFactory();
        
        // 新しい承認情報の作成
        GoogleCredential credential = new GoogleCredential.Builder()
                .setJsonFactory(jsonFactory)
                .setTransport(httpTransport)
                .setClientSecrets(googleClientSecrets)
                .build();
        
        // DBから取得したアクセストークンとリフレッシュトークンをセット
        credential.setAccessToken(entity.accessToken).setRefreshToken(entity.refreshToken);

        return credential;
    }
    
    /**
     * Build an authorization flow and store it as a static class attribute.
     *
     * @return GoogleAuthorizationCodeFlow instance.
     * @throws IOException Unable to load client_secrets.json.
     */
    private GoogleAuthorizationCodeFlow getFlow() throws IOException {
        if (flow == null) {
            HttpTransport httpTransport = new NetHttpTransport();
            JacksonFactory jsonFactory = new JacksonFactory();

            flow = new GoogleAuthorizationCodeFlow.Builder(httpTransport, jsonFactory, googleClientSecrets, SCOPES)
                    .setAccessType("offline").setApprovalPrompt("force").build();
        }
        return flow;
    }

    /**
     * OAuth2承認用URLの取得
     *
     * @param emailAddress User's e-mail address.
     * @return Authorization URL to redirect the user to.
     * @throws IOException Unable to load client_secrets.json.
     */
    public String getAuthorizationUrl(String emailAddress) throws IOException {
        GoogleAuthorizationCodeRequestUrl urlBuilder =
                getFlow().newAuthorizationUrl().setRedirectUri(googleClientSecrets.getDetails().getRedirectUris().get(0));
        urlBuilder.set("user_id", emailAddress);
        return urlBuilder.build();
    }

    /**
     * OAuth2承認用URLの取得
     * @return
     * @throws IOException 
     */
    public String getAuthorizationUrl() throws IOException {
        GoogleAuthorizationCodeRequestUrl urlBuilder =
                getFlow().newAuthorizationUrl().setRedirectUri(googleClientSecrets.getDetails().getRedirectUris().get(0));
        return urlBuilder.build();
    }

    /**
     * Exchange an authorization code for OAuth 2.0 credentials.
     *
     * @param authorizationCode Authorization code to exchange for OAuth 2.0
     * credentials.
     * @return OAuth 2.0 credentials.
     * @throws CodeExchangeException An error occurred.
     */
    public Credential exchangeCode(String authorizationCode)
            throws CodeExchangeException {
        try {
            GoogleAuthorizationCodeFlow flowTemp = getFlow();
            GoogleTokenResponse response =
                    flowTemp.newTokenRequest(authorizationCode).setRedirectUri(googleClientSecrets.getDetails().getRedirectUris().get(0)).execute();
            
            return flowTemp.createAndStoreCredential(response, null);
        } catch (IOException e) {
            System.err.println("An error occurred: " + e);
            throw new CodeExchangeException(null);
        }
    }
    
    /**
     * 無効トークンチェック
     * @return 
     */
    private boolean isInvalidToken(String accessToken) throws Exception {
        
        String json;
        try {
            json = getResponseJson("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=" + accessToken);
            
        } catch (Exception e) {
            return true;
        }
        
        if(json == null) {
            return true;
        }
        
        Type type = new TypeToken<Oauth2Model>() {}.getType();
        Gson gson = new Gson();
        
        Oauth2Model oauth2Model = (Oauth2Model)gson.fromJson(json, type);
        
        return oauth2Model.getError() != null && oauth2Model.getError().equals("invalid_token");
    }
    
    /**
     * リフレッシュトークンから新しいアクセストークンを取得
     * @param entity
     * @return
     * @throws Exception 
     */
    private String getNewAccessTokenByRefreshToken(OauthMstEntity entity) throws Exception {
        
        StringBuilder psb = new StringBuilder();
        psb.append("client_id=");
        psb.append(googleClientSecrets.getDetails().getClientId());
        psb.append("&");
        psb.append("client_secret=");
        psb.append(googleClientSecrets.getDetails().getClientSecret());
        psb.append("&");
        psb.append("refresh_token=");
        psb.append(entity.refreshToken);
        psb.append("&");
        psb.append("grant_type=");
        psb.append("refresh_token");
        
        Type type = new TypeToken<Oauth2Model>() {}.getType();
        Oauth2Model oauth2Model = (Oauth2Model)postGetResponseJson(
                new URL(googleClientSecrets.getDetails().getTokenUri()),
                psb.toString(),
                type);
        
        System.out.println("New Access Token:" + oauth2Model.getAccessToken() + "/" + oauth2Model.getTokenType());
        
        return oauth2Model.getAccessToken();
    }
    
    /**
     * HTTP GET
     * @param urlString
     * @return
     * @throws Exception 
     */
    private static String getResponseJson(String urlString) throws Exception {
        BufferedReader reader = null;
        try {
            URL url = new URL(urlString);
            HttpURLConnection http = (HttpURLConnection) url.openConnection();
            http.setRequestMethod("GET");
            
            // 接続
            http.connect();
            
            if(http.getResponseCode() == 400) {
                throw new Exception();
            }
            
            reader = new BufferedReader(new InputStreamReader(http.getInputStream(), "UTF-8"));
            StringBuilder buffer = new StringBuilder();
            int read;
            char[] chars = new char[1024];
            while ((read = reader.read(chars)) != -1) {
                buffer.append(chars, 0, read);
            }

            return buffer.toString();
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
    }
    
    /**
     * Json POST
     * @param postURL
     * @param postString
     * @param responseModelType
     * @return
     * @throws Exception 
     */
    private static Object postGetResponseJson(URL postURL, String postString, Type responseModelType) throws Exception {
        HttpURLConnection http = (HttpURLConnection)postURL.openConnection();
        http.setRequestMethod("POST");

        // 送信する指定
        http.setDoOutput(true);
        try (OutputStreamWriter ow = new OutputStreamWriter(http.getOutputStream(), "utf-8"); BufferedWriter bw = new BufferedWriter(ow)) {

            //POSTの内容を書き出す
            bw.write(postString);
            bw.flush();
        }
        String json;
        Gson gson;
        try (InputStreamReader ir = new InputStreamReader(http.getInputStream(), "utf-8"); BufferedReader br1 = new BufferedReader(ir)) {
            StringBuilder rsb = new StringBuilder();
            int read;
            char[] chars = new char[1024];
            while ((read = br1.read(chars)) != -1) {
                rsb.append(chars, 0, read);
            }
            json = rsb.toString();
            gson = new Gson();
        }

        return gson.fromJson(json, responseModelType);
    }
    
    /**
     * リダイレクトURIの取得
     * @return 
     */
    public String getRedirectUri() {
        return googleClientSecrets.getDetails().getRedirectUris().get(0);
    }
}
